1use super::bse::*;
3use super::dsc::*;
4use crate::ext::io::*;
5use crate::scripts::base::*;
6use crate::types::*;
7use crate::utils::encoding::encode_string;
8use crate::utils::struct_pack::*;
9use anyhow::Result;
10use msg_tool_macro::*;
11use std::collections::HashMap;
12use std::io::{Read, Seek, SeekFrom, Write};
13use std::sync::{Arc, Mutex};
14
15#[derive(Debug)]
16pub struct BgiArchiveBuilder {}
18
19impl BgiArchiveBuilder {
20 pub const fn new() -> Self {
22 BgiArchiveBuilder {}
23 }
24}
25
26impl ScriptBuilder for BgiArchiveBuilder {
27 fn default_encoding(&self) -> Encoding {
28 Encoding::Cp932
29 }
30
31 fn default_archive_encoding(&self) -> Option<Encoding> {
32 Some(Encoding::Cp932)
33 }
34
35 fn build_script(
36 &self,
37 data: Vec<u8>,
38 filename: &str,
39 _encoding: Encoding,
40 archive_encoding: Encoding,
41 config: &ExtraConfig,
42 _archive: Option<&Box<dyn Script>>,
43 ) -> Result<Box<dyn Script>> {
44 Ok(Box::new(BgiArchive::new(
45 MemReader::new(data),
46 archive_encoding,
47 config,
48 filename,
49 )?))
50 }
51
52 fn build_script_from_file(
53 &self,
54 filename: &str,
55 _encoding: Encoding,
56 archive_encoding: Encoding,
57 config: &ExtraConfig,
58 _archive: Option<&Box<dyn Script>>,
59 ) -> Result<Box<dyn Script>> {
60 if filename == "-" {
61 let data = crate::utils::files::read_file(filename)?;
62 Ok(Box::new(BgiArchive::new(
63 MemReader::new(data),
64 archive_encoding,
65 config,
66 filename,
67 )?))
68 } else {
69 let f = std::fs::File::open(filename)?;
70 let reader = std::io::BufReader::new(f);
71 Ok(Box::new(BgiArchive::new(
72 reader,
73 archive_encoding,
74 config,
75 filename,
76 )?))
77 }
78 }
79
80 fn build_script_from_reader(
81 &self,
82 reader: Box<dyn ReadSeek>,
83 filename: &str,
84 _encoding: Encoding,
85 archive_encoding: Encoding,
86 config: &ExtraConfig,
87 _archive: Option<&Box<dyn Script>>,
88 ) -> Result<Box<dyn Script>> {
89 Ok(Box::new(BgiArchive::new(
90 reader,
91 archive_encoding,
92 config,
93 filename,
94 )?))
95 }
96
97 fn extensions(&self) -> &'static [&'static str] {
98 &["arc"]
99 }
100
101 fn script_type(&self) -> &'static ScriptType {
102 &ScriptType::BGIArcV1
103 }
104
105 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
106 if buf_len >= 12 && buf.starts_with(b"PackFile ") {
107 return Some(10);
108 }
109 None
110 }
111
112 fn is_archive(&self) -> bool {
113 true
114 }
115
116 fn create_archive(
117 &self,
118 filename: &str,
119 files: &[&str],
120 encoding: Encoding,
121 config: &ExtraConfig,
122 ) -> Result<Box<dyn Archive>> {
123 let f = std::fs::File::create(filename)?;
124 let writer = std::io::BufWriter::new(f);
125 Ok(Box::new(BgiArchiveWriter::new(
126 writer, files, encoding, config,
127 )?))
128 }
129}
130
131#[derive(Clone, Debug, StructPack, StructUnpack)]
132struct BgiFileHeader {
133 #[fstring = 16]
134 filename: String,
135 offset: u32,
136 size: u32,
137 #[fvec = 8]
138 _padding: Vec<u8>,
139}
140
141struct Entry<T: Read + Seek> {
142 header: BgiFileHeader,
143 reader: Arc<Mutex<T>>,
144 pos: usize,
145 base_offset: u64,
146 script_type: Option<ScriptType>,
147}
148
149impl<T: Read + Seek> ArchiveContent for Entry<T> {
150 fn name(&self) -> &str {
151 &self.header.filename
152 }
153
154 fn script_type(&self) -> Option<&ScriptType> {
155 self.script_type.as_ref()
156 }
157}
158
159impl<T: Read + Seek> Read for Entry<T> {
160 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
161 let mut reader = self.reader.lock().map_err(|e| {
162 std::io::Error::new(
163 std::io::ErrorKind::Other,
164 format!("Failed to lock mutex: {}", e),
165 )
166 })?;
167 reader.seek(SeekFrom::Start(
168 self.base_offset + self.header.offset as u64 + self.pos as u64,
169 ))?;
170 let bytes_read = buf.len().min(self.header.size as usize - self.pos);
171 if bytes_read == 0 {
172 return Ok(0);
173 }
174 let bytes_read = reader.read(&mut buf[..bytes_read])?;
175 self.pos += bytes_read;
176 Ok(bytes_read)
177 }
178}
179
180impl<T: Read + Seek> Seek for Entry<T> {
181 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
182 let new_pos = match pos {
183 SeekFrom::Start(offset) => offset as usize,
184 SeekFrom::End(offset) => {
185 if offset < 0 {
186 if (-offset) as usize > self.header.size as usize {
187 return Err(std::io::Error::new(
188 std::io::ErrorKind::InvalidInput,
189 "Seek from end exceeds file length",
190 ));
191 }
192 self.header.size as usize - (-offset) as usize
193 } else {
194 self.header.size as usize + offset as usize
195 }
196 }
197 SeekFrom::Current(offset) => {
198 if offset < 0 {
199 if (-offset) as usize > self.pos {
200 return Err(std::io::Error::new(
201 std::io::ErrorKind::InvalidInput,
202 "Seek from current exceeds current position",
203 ));
204 }
205 self.pos.saturating_sub((-offset) as usize)
206 } else {
207 self.pos + offset as usize
208 }
209 }
210 };
211 self.pos = new_pos;
212 Ok(self.pos as u64)
213 }
214
215 fn stream_position(&mut self) -> std::io::Result<u64> {
216 Ok(self.pos as u64)
217 }
218}
219
220#[derive(Debug)]
221pub struct BgiArchive<T: Read + Seek + std::fmt::Debug> {
223 reader: Arc<Mutex<T>>,
224 entries: Vec<BgiFileHeader>,
225 base_offset: u64,
226 #[cfg(feature = "bgi-img")]
227 is_sysgrp_arc: bool,
228}
229
230impl<T: Read + Seek + std::fmt::Debug> BgiArchive<T> {
231 pub fn new(
238 mut reader: T,
239 archive_encoding: Encoding,
240 _config: &ExtraConfig,
241 _filename: &str,
242 ) -> Result<Self> {
243 let mut header = [0u8; 12];
244 reader.read_exact(&mut header)?;
245 if !header.starts_with(b"PackFile ") {
246 return Err(anyhow::anyhow!("Invalid BGI archive header"));
247 }
248
249 let file_count = reader.read_u32()?;
250 let mut entries = Vec::with_capacity(file_count as usize);
251 for _ in 0..file_count {
252 let entry = BgiFileHeader::unpack(&mut reader, false, archive_encoding)?;
253 entries.push(entry);
254 }
255
256 #[cfg(feature = "bgi-img")]
257 let is_sysgrp_arc = _config.bgi_is_sysgrp_arc.unwrap_or_else(|| {
258 std::path::Path::new(&_filename.to_lowercase())
259 .file_name()
260 .map(|f| f == "sysgrp.arc")
261 .unwrap_or(false)
262 });
263
264 Ok(BgiArchive {
265 reader: Arc::new(Mutex::new(reader)),
266 entries,
267 base_offset: 16 + (file_count as u64 * 32),
268 #[cfg(feature = "bgi-img")]
269 is_sysgrp_arc,
270 })
271 }
272}
273
274impl<T: Read + Seek + std::fmt::Debug + 'static> Script for BgiArchive<T> {
275 fn default_output_script_type(&self) -> OutputScriptType {
276 OutputScriptType::Json
277 }
278
279 fn default_format_type(&self) -> FormatOptions {
280 FormatOptions::None
281 }
282
283 fn is_archive(&self) -> bool {
284 true
285 }
286
287 fn iter_archive_filename<'a>(
288 &'a self,
289 ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
290 Ok(Box::new(
291 self.entries.iter().map(|e| Ok(e.filename.clone())),
292 ))
293 }
294
295 fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
296 Ok(Box::new(self.entries.iter().map(|e| Ok(e.offset as u64))))
297 }
298
299 fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
300 if index >= self.entries.len() {
301 return Err(anyhow::anyhow!(
302 "Index out of bounds: {} (max: {})",
303 index,
304 self.entries.len()
305 ));
306 }
307 let entry = &self.entries[index];
308 let mut entry = Entry {
309 header: entry.clone(),
310 reader: self.reader.clone(),
311 pos: 0,
312 base_offset: self.base_offset,
313 script_type: None,
314 };
315 let mut buf = [0u8; 32];
316 match entry.read(&mut buf) {
317 Ok(_) => {}
318 Err(e) => {
319 return Err(anyhow::anyhow!(
320 "Failed to read entry '{}': {}",
321 entry.header.filename,
322 e
323 ));
324 }
325 }
326 entry.pos = 0;
327 if buf.starts_with(b"DSC FORMAT 1.00") {
328 let data = match entry.data() {
329 Ok(data) => data,
330 Err(e) => {
331 return Err(anyhow::anyhow!(
332 "Failed to read DSC data for '{}': {}",
333 entry.header.filename,
334 e
335 ));
336 }
337 };
338 entry.pos = 0;
339 let dsc = match DscDecoder::new(&data) {
340 Ok(dsc) => dsc,
341 Err(e) => {
342 return Err(anyhow::anyhow!(
343 "Failed to create DSC decoder for '{}': {}",
344 entry.header.filename,
345 e
346 ));
347 }
348 };
349 let decoded = match dsc.unpack() {
350 Ok(decoded) => decoded,
351 Err(e) => {
352 return Err(anyhow::anyhow!(
353 "Failed to unpack DSC data for '{}': {}",
354 entry.header.filename,
355 e
356 ));
357 }
358 };
359 let reader = MemReader::new(decoded);
360 if reader.data.starts_with(b"BSE 1.") {
361 match BseReader::new(reader, detect_script_type, &entry.header.filename) {
362 Ok(bse_reader) => {
363 return Ok(Box::new(bse_reader));
364 }
365 Err(e) => {
366 return Err(anyhow::anyhow!(
367 "Failed to create BSE reader for '{}': {}",
368 entry.header.filename,
369 e
370 ));
371 }
372 };
373 }
374 return Ok(Box::new(MemEntry {
375 name: entry.header.filename.clone(),
376 data: reader,
377 #[cfg(feature = "bgi-img")]
378 detect: if self.is_sysgrp_arc {
379 detect_script_type_sysgrp
380 } else {
381 detect_script_type
382 },
383 #[cfg(not(feature = "bgi-img"))]
384 detect: detect_script_type,
385 }));
386 }
387 if buf.starts_with(b"BSE 1.") {
388 let filename = entry.header.filename.clone();
389 #[cfg(feature = "bgi-img")]
390 let detect = if self.is_sysgrp_arc {
391 detect_script_type_sysgrp
392 } else {
393 detect_script_type
394 };
395 #[cfg(not(feature = "bgi-img"))]
396 let detect = detect_script_type;
397 match BseReader::new(entry, detect, &filename) {
398 Ok(mut bse_reader) => {
399 if bse_reader.is_dsc() {
400 let data = match bse_reader.data() {
401 Ok(data) => data,
402 Err(e) => {
403 return Err(anyhow::anyhow!(
404 "Failed to read BSE data for '{}': {}",
405 &filename,
406 e
407 ));
408 }
409 };
410 let dsc = match DscDecoder::new(&data) {
411 Ok(dsc) => dsc,
412 Err(e) => {
413 return Err(anyhow::anyhow!(
414 "Failed to create DSC decoder for '{}': {}",
415 &filename,
416 e
417 ));
418 }
419 };
420 let decoded = match dsc.unpack() {
421 Ok(decoded) => decoded,
422 Err(e) => {
423 return Err(anyhow::anyhow!(
424 "Failed to unpack DSC data for '{}': {}",
425 &filename,
426 e
427 ));
428 }
429 };
430 let reader = MemReader::new(decoded);
431 return Ok(Box::new(MemEntry {
432 name: filename,
433 data: reader,
434 detect,
435 }));
436 }
437 return Ok(Box::new(bse_reader));
438 }
439 Err(e) => {
440 return Err(anyhow::anyhow!(
441 "Failed to create BSE reader for '{}': {}",
442 &filename,
443 e
444 ));
445 }
446 };
447 }
448 #[cfg(feature = "bgi-img")]
449 if self.is_sysgrp_arc {
450 entry.script_type = Some(ScriptType::BGIImg);
451 } else {
452 entry.script_type =
453 detect_script_type(&buf, buf.len(), &entry.header.filename).cloned();
454 }
455 #[cfg(not(feature = "bgi-img"))]
456 {
457 entry.script_type =
458 detect_script_type(&buf, buf.len(), &entry.header.filename).cloned();
459 }
460 Ok(Box::new(entry))
461 }
462}
463
464struct MemEntry<F: Fn(&[u8], usize, &str) -> Option<&'static ScriptType>> {
465 name: String,
466 data: MemReader,
467 detect: F,
468}
469
470impl<F: Fn(&[u8], usize, &str) -> Option<&'static ScriptType>> Read for MemEntry<F> {
471 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
472 self.data.read(buf)
473 }
474}
475
476impl<F: Fn(&[u8], usize, &str) -> Option<&'static ScriptType>> ArchiveContent for MemEntry<F> {
477 fn name(&self) -> &str {
478 &self.name
479 }
480
481 fn script_type(&self) -> Option<&ScriptType> {
482 (self.detect)(&self.data.data, self.data.data.len(), &self.name)
483 }
484
485 fn data(&mut self) -> Result<Vec<u8>> {
486 Ok(self.data.data.clone())
487 }
488
489 fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + 'a>> {
490 Ok(Box::new(&mut self.data))
491 }
492}
493
494fn detect_script_type(buf: &[u8], buf_len: usize, filename: &str) -> Option<&'static ScriptType> {
495 if buf_len >= 28 && buf.starts_with(b"BurikoCompiledScriptVer1.00\0") {
496 return Some(&ScriptType::BGI);
497 }
498 #[cfg(feature = "bgi-img")]
499 if buf_len >= 16 && buf.starts_with(b"CompressedBG___") {
500 return Some(&ScriptType::BGICbg);
501 }
502 #[cfg(feature = "bgi-audio")]
503 if buf_len >= 8 && buf[4..].starts_with(b"bw ") {
504 return Some(&ScriptType::BGIAudio);
505 }
506 let filename = filename.to_lowercase();
507 if filename.ends_with("._bp") {
508 return Some(&ScriptType::BGIBp);
509 } else if filename.ends_with("._bsi") {
510 return Some(&ScriptType::BGIBsi);
511 }
512 None
513}
514
515#[cfg(feature = "bgi-img")]
516fn detect_script_type_sysgrp(
517 _buf: &[u8],
518 _buf_len: usize,
519 _filename: &str,
520) -> Option<&'static ScriptType> {
521 Some(&ScriptType::BGIImg)
522}
523
524pub struct BgiArchiveWriter<T: Write + Seek> {
526 writer: T,
527 headers: HashMap<String, BgiFileHeader>,
528 compress_file: bool,
529 encoding: Encoding,
530 min_len: usize,
531}
532
533impl<T: Write + Seek> BgiArchiveWriter<T> {
534 pub fn new(
541 mut writer: T,
542 files: &[&str],
543 encoding: Encoding,
544 config: &ExtraConfig,
545 ) -> Result<Self> {
546 writer.write_all(b"PackFile ")?;
547 let file_count = files.len() as u32;
548 writer.write_u32(file_count)?;
549 let mut headers = HashMap::new();
550 for file in files {
551 let header = BgiFileHeader {
552 filename: file.to_string(),
553 offset: 0,
554 size: 0,
555 _padding: vec![0; 8],
556 };
557 header.pack(&mut writer, false, encoding)?;
558 headers.insert(file.to_string(), header);
559 }
560 Ok(BgiArchiveWriter {
561 writer,
562 headers,
563 compress_file: config.bgi_compress_file,
564 encoding,
565 min_len: config.bgi_compress_min_len,
566 })
567 }
568}
569
570impl<T: Write + Seek> Archive for BgiArchiveWriter<T> {
571 fn new_file<'a>(&'a mut self, name: &str) -> Result<Box<dyn WriteSeek + 'a>> {
572 let entry = self
573 .headers
574 .get_mut(name)
575 .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
576 if entry.offset != 0 || entry.size != 0 {
577 return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
578 }
579 self.writer.seek(SeekFrom::End(0))?;
580 entry.offset = self.writer.stream_position()? as u32;
581 let file = BgiArchiveFile {
582 header: entry,
583 writer: &mut self.writer,
584 pos: 0,
585 };
586 Ok(if self.compress_file {
587 Box::new(BgiArchiveFileWithDsc::new(file, self.min_len))
588 } else {
589 Box::new(file)
590 })
591 }
592
593 fn write_header(&mut self) -> Result<()> {
594 self.writer.seek(SeekFrom::Start(0x10))?;
595 let base_offset = self.headers.len() as u32 * 0x20 + 16;
596 let mut files = self.headers.iter_mut().map(|(_, d)| d).collect::<Vec<_>>();
597 files.sort_by_key(|f| f.offset);
598 for file in files {
599 file.offset -= base_offset;
600 file.pack(&mut self.writer, false, self.encoding)?;
601 }
602 Ok(())
603 }
604}
605
606pub struct BgiArchiveFile<'a, T: Write + Seek> {
608 header: &'a mut BgiFileHeader,
609 writer: &'a mut T,
610 pos: usize,
611}
612
613impl<'a, T: Write + Seek> Write for BgiArchiveFile<'a, T> {
614 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
615 self.writer
616 .seek(SeekFrom::Start(self.header.offset as u64 + self.pos as u64))?;
617 let bytes_written = self.writer.write(buf)?;
618 self.pos += bytes_written;
619 self.header.size = self.header.size.max(self.pos as u32);
620 Ok(bytes_written)
621 }
622
623 fn flush(&mut self) -> std::io::Result<()> {
624 self.writer.flush()
625 }
626}
627
628impl<'a, T: Write + Seek> Seek for BgiArchiveFile<'a, T> {
629 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
630 let new_pos = match pos {
631 SeekFrom::Start(offset) => offset as usize,
632 SeekFrom::End(offset) => {
633 if offset < 0 {
634 if (-offset) as usize > self.header.size as usize {
635 return Err(std::io::Error::new(
636 std::io::ErrorKind::InvalidInput,
637 "Seek from end exceeds file length",
638 ));
639 }
640 self.header.size as usize - (-offset) as usize
641 } else {
642 self.header.size as usize + offset as usize
643 }
644 }
645 SeekFrom::Current(offset) => {
646 if offset < 0 {
647 if (-offset) as usize > self.pos {
648 return Err(std::io::Error::new(
649 std::io::ErrorKind::InvalidInput,
650 "Seek from current exceeds current position",
651 ));
652 }
653 self.pos.saturating_sub((-offset) as usize)
654 } else {
655 self.pos + offset as usize
656 }
657 }
658 };
659 self.pos = new_pos;
660 Ok(self.pos as u64)
661 }
662}
663
664pub struct BgiArchiveFileWithDsc<'a, T: Write + Seek> {
666 writer: BgiArchiveFile<'a, T>,
667 buf: MemWriter,
668 min_len: usize,
669}
670
671impl<'a, T: Write + Seek> BgiArchiveFileWithDsc<'a, T> {
672 pub fn new(writer: BgiArchiveFile<'a, T>, min_len: usize) -> Self {
677 BgiArchiveFileWithDsc {
678 writer,
679 buf: MemWriter::new(),
680 min_len,
681 }
682 }
683}
684
685impl<'a, T: Write + Seek> Write for BgiArchiveFileWithDsc<'a, T> {
686 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
687 self.buf.write(buf)
688 }
689
690 fn flush(&mut self) -> std::io::Result<()> {
691 self.buf.flush()
692 }
693}
694
695impl<'a, T: Write + Seek> Seek for BgiArchiveFileWithDsc<'a, T> {
696 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
697 self.buf.seek(pos)
698 }
699
700 fn stream_position(&mut self) -> std::io::Result<u64> {
701 self.buf.stream_position()
702 }
703
704 fn rewind(&mut self) -> std::io::Result<()> {
705 self.buf.rewind()
706 }
707}
708
709impl<'a, T: Write + Seek> Drop for BgiArchiveFileWithDsc<'a, T> {
710 fn drop(&mut self) {
711 let buf = self.buf.as_slice();
712 let encoder = DscEncoder::new(&mut self.writer, self.min_len);
713 if let Err(e) = encoder.pack(&buf) {
714 eprintln!("Failed to write DSC data: {}", e);
715 crate::COUNTER.inc_error();
716 }
717 }
718}